package com.slim.utils;
/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * This class is using the scp client to transfer data and information for the repository.
 * <P>
 * * It is based on the SCPClient from the ganymed ssh library from Christian Plattner, released under a BSD style
 * license.
 * <P>
 * * To minimize the dependency to the ssh library and because we needed some additional functionality, we decided to
 * copy'n'paste the single class rather than to inherit or delegate it somehow.
 * <P>
 * * Nevertheless credit should go to the original author.
 */
public class Scp {
	private static final int MODE_LENGTH = 4;

	private static final int SEND_FILE_BUFFER_LENGTH = 40000;

	private static final int SEND_BYTES_BUFFER_LENGTH = 512;

	private static final int MIN_TLINE_LENGTH = 8;

	private static final int CLINE_SPACE_INDEX2 = 5;

	private static final int CLINE_SPACE_INDEX1 = 4;

	private static final int MIN_C_LINE_LENGTH = 8;

	private static final int DEFAULT_LINE_BUFFER_LENGTH = 30;

	private static final int BUFFER_SIZE = 64 * 1024;

	/*
	 * Maximum length authorized for scp lines. This is a random limit - if your path names are longer, then adjust it.
	 */
	private static final int MAX_SCP_LINE_LENGTH = 8192;

	private final Session session;

	public class FileInfo {
		private String filename;

		private long length;

		private long lastModified;

		/**
		 * @param filename
		 *            The filename to set.
		 */
		public void setFilename(String filename) {
			this.filename = filename;
		}

		/**
		 * @return Returns the filename.
		 */
		public String getFilename() {
			return filename;
		}

		/**
		 * @param length
		 *            The length to set.
		 */
		public void setLength(long length) {
			this.length = length;
		}

		/**
		 * @return Returns the length.
		 */
		public long getLength() {
			return length;
		}

		/**
		 * @param lastModified
		 *            The lastModified to set.
		 */
		public void setLastModified(long lastModified) {
			this.lastModified = lastModified;
		}

		/**
		 * @return Returns the lastModified.
		 */
		public long getLastModified() {
			return lastModified;
		}
	}

	public Scp(Session session) {
		if (session == null) {
			throw new IllegalArgumentException("Cannot accept null argument!");
		}
		this.session = session;
	}

	private void readResponse(InputStream is) throws IOException, Exception {
		if (is.available() > 0) {
			int c = is.read();

			if (c == 0) {
				return;
			}

			if (c == -1) {
				throw new Exception("Remote scp terminated unexpectedly.");
			}

			if ((c != 1) && (c != 2)) {
				throw new Exception("Remote scp sent illegal error code.");
			}

			if (c == 2) {
				throw new Exception("Remote scp terminated with error.");
			}

			String err = receiveLine(is);
			throw new Exception("Remote scp terminated with error (" + err + ").");
		}
	}

	private String receiveLine(InputStream is) throws IOException, Exception {
		StringBuffer sb = new StringBuffer(DEFAULT_LINE_BUFFER_LENGTH);

		while (true) {

			if (sb.length() > MAX_SCP_LINE_LENGTH) {
				throw new Exception("Remote scp sent a too long line");
			}

			int c = is.read();

			if (c < 0) {
				throw new Exception("Remote scp terminated unexpectedly.");
			}

			if (c == '\n') {
				break;
			}

			sb.append((char) c);

		}
		return sb.toString();
	}

	private void parseCLine(String line, FileInfo fileInfo) throws Exception {
		/* Minimum line: "xxxx y z" ---> 8 chars */

		long len;

		if (line.length() < MIN_C_LINE_LENGTH) {
			throw new Exception("Malformed C line sent by remote SCP binary, line too short.");
		}

		if ((line.charAt(CLINE_SPACE_INDEX1) != ' ') || (line.charAt(CLINE_SPACE_INDEX2) == ' ')) {
			throw new Exception("Malformed C line sent by remote SCP binary.");
		}

		int lengthNameSep = line.indexOf(' ', CLINE_SPACE_INDEX2);

		if (lengthNameSep == -1) {
			throw new Exception("Malformed C line sent by remote SCP binary.");
		}

		String lengthSubstring = line.substring(CLINE_SPACE_INDEX2, lengthNameSep);
		String nameSubstring = line.substring(lengthNameSep + 1);

		if ((lengthSubstring.length() <= 0) || (nameSubstring.length() <= 0)) {
			throw new Exception("Malformed C line sent by remote SCP binary.");
		}

		if ((CLINE_SPACE_INDEX2 + 1 + lengthSubstring.length() + nameSubstring.length()) != line.length()) {
			throw new Exception("Malformed C line sent by remote SCP binary.");
		}

		try {
			len = Long.parseLong(lengthSubstring);
		} catch (NumberFormatException e) {
			throw new Exception("Malformed C line sent by remote SCP binary, cannot parse file length.");
		}

		if (len < 0) {
			throw new Exception("Malformed C line sent by remote SCP binary, illegal file length.");
		}

		fileInfo.setLength(len);
		fileInfo.setFilename(nameSubstring);
	}

	private void parseTLine(String line, FileInfo fileInfo) throws Exception {
		/* Minimum line: "0 0 0 0" ---> 8 chars */

		long modtime;
		long firstMsec;
		long atime;
		long secondMsec;

		if (line.length() < MIN_TLINE_LENGTH) {
			throw new Exception("Malformed T line sent by remote SCP binary, line too short.");
		}

		int firstMsecBegin = line.indexOf(" ") + 1;
		if (firstMsecBegin == 0 || firstMsecBegin >= line.length()) {
			throw new Exception("Malformed T line sent by remote SCP binary, line not enough data.");
		}

		int atimeBegin = line.indexOf(" ", firstMsecBegin + 1) + 1;
		if (atimeBegin == 0 || atimeBegin >= line.length()) {
			throw new Exception("Malformed T line sent by remote SCP binary, line not enough data.");
		}

		int secondMsecBegin = line.indexOf(" ", atimeBegin + 1) + 1;
		if (secondMsecBegin == 0 || secondMsecBegin >= line.length()) {
			throw new Exception("Malformed T line sent by remote SCP binary, line not enough data.");
		}

		try {
			modtime = Long.parseLong(line.substring(0, firstMsecBegin - 1));
			firstMsec = Long.parseLong(line.substring(firstMsecBegin, atimeBegin - 1));
			atime = Long.parseLong(line.substring(atimeBegin, secondMsecBegin - 1));
			secondMsec = Long.parseLong(line.substring(secondMsecBegin));
		} catch (NumberFormatException e) {
			// LOGGER.error(e);
			throw new Exception("Malformed C line sent by remote SCP binary, cannot parse file length.");
		}

		if (modtime < 0 || firstMsec < 0 || atime < 0 || secondMsec < 0) {
			throw new Exception("Malformed C line sent by remote SCP binary, illegal file length.");
		}

		fileInfo.setLastModified(modtime);
	}

	private void sendFile(Channel channel, String localFile, String remoteName, String mode)
		throws IOException,
		Exception {
		byte[] buffer = new byte[BUFFER_SIZE];

		OutputStream os = new BufferedOutputStream(channel.getOutputStream(), SEND_FILE_BUFFER_LENGTH);
		InputStream is = new BufferedInputStream(channel.getInputStream(), SEND_BYTES_BUFFER_LENGTH);

		try {
			if (channel.isConnected()) {
				channel.start();
			} else {
				channel.connect();
			}
		} catch (JSchException e1) {
			throw (IOException) new IOException("Channel connection problems").initCause(e1);
		}

		readResponse(is);

		File f = new File(localFile);
		long remain = f.length();

		String cMode = mode;
		if (cMode == null) {
			cMode = "0600";
		}
		String cline = "C" + cMode + " " + remain + " " + remoteName + "\n";

		os.write(cline.getBytes());
		os.flush();

		readResponse(is);

		FileInputStream fis = null;

		try {
			fis = new FileInputStream(f);

			while (remain > 0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
				}
				int trans;
				if (remain > buffer.length) {
					trans = buffer.length;
				} else {
					trans = (int) remain;
				}
				if (fis.read(buffer, 0, trans) != trans) {
					throw new IOException("Cannot read enough from local file " + localFile);
				}

				os.write(buffer, 0, trans);

				remain -= trans;
			}

			fis.close();
		} catch (Exception e) {
			if (fis != null) {
				fis.close();
			}
			// LOGGER.error(e);
			throw new Exception(e);
		}

		os.write(0);
		os.flush();

		readResponse(is);

		os.write("E\n".getBytes());
		os.flush();
	}

	/**
	 * Receive a file via scp and store it in a stream
	 * 
	 * @param channel
	 *            ssh channel to use
	 * @param file
	 *            to receive from remote
	 * @param target
	 *            to store file into (if null, get only file info)
	 * @return file information of the file we received
	 * @throws IOException
	 *             in case of network or protocol trouble
	 * @throws Exception
	 *             in case of problems on the target system (connection is fine)
	 */
	private FileInfo receiveStream(Channel channel, String file, OutputStream targetStream)
		throws IOException,
		Exception {
		byte[] buffer = new byte[BUFFER_SIZE];

		OutputStream os = channel.getOutputStream();
		InputStream is = channel.getInputStream();
		try {
			if (channel.isConnected()) {
				channel.start();
			} else {
				channel.connect();
			}
		} catch (JSchException e1) {
			throw (IOException) new IOException("Channel connection problems").initCause(e1);
		}
		os.write(0x0);
		os.flush();

		FileInfo fileInfo = new FileInfo();

		while (true) {
			int c = is.read();
			if (c < 0) {
				throw new Exception("Remote scp terminated unexpectedly.");
			}

			String line = receiveLine(is);

			if (c == 'T') {
				parseTLine(line, fileInfo);
				os.write(0x0);
				os.flush();
				continue;
			}
			if ((c == 1) || (c == 2)) {
				throw new Exception("Remote SCP error: " + line);
			}

			if (c == 'C') {
				parseCLine(line, fileInfo);
				break;
			}
			throw new Exception("Remote SCP error: " + ((char) c) + line);
		}
		if (targetStream != null) {

			os.write(0x0);
			os.flush();

			try {
				long remain = fileInfo.getLength();

				while (remain > 0) {
					int trans;
					if (remain > buffer.length) {
						trans = buffer.length;
					} else {
						trans = (int) remain;
					}

					int thisTimeReceived = is.read(buffer, 0, trans);

					if (thisTimeReceived < 0) {
						throw new IOException("Remote scp terminated connection unexpectedly");
					}

					targetStream.write(buffer, 0, thisTimeReceived);

					remain -= thisTimeReceived;
				}

				targetStream.close();
			} catch (IOException e) {
				if (targetStream != null) {
					targetStream.close();
				}
				// LOGGER.error(e);
				throw (e);
			}

			readResponse(is);

			os.write(0x0);
			os.flush();
		}
		return fileInfo;
	}

	/**
	 * @return
	 * @throws JSchException
	 */
	private ChannelExec getExecChannel() throws JSchException {
		ChannelExec channel;
		channel = (ChannelExec) session.openChannel("exec");
		return channel;
	}

	/**
	 * Copy a local file to a remote site, uses the specified mode when creating the file on the remote side.
	 * 
	 * @param localFile
	 *            Path and name of local file. Must be absolute.
	 * @param remoteTargetDir
	 *            Remote target directory where the file has to end up (optional)
	 * @param remoteTargetName
	 *            file name to use on the target system
	 * @param mode
	 *            a four digit string (e.g., 0644, see "man chmod", "man open")
	 * @throws IOException
	 *             in case of network problems
	 * @throws Exception
	 *             in case of problems on the target system (connection ok)
	 */
	public void put(String localFile, String remoteTargetDir, String remoteTargetName, String mode) throws Exception {
		ChannelExec channel = null;

		if ((localFile == null) || (remoteTargetName == null)) {
			throw new IllegalArgumentException("Null argument.");
		}

		if (mode != null) {
			if (mode.length() != MODE_LENGTH) {
				throw new IllegalArgumentException("Invalid mode.");
			}

			for (int i = 0; i < mode.length(); i++) {
				if (!Character.isDigit(mode.charAt(i))) {
					throw new IllegalArgumentException("Invalid mode.");
				}
			}
		}

		String cmd = "scp -t ";
		if (mode != null) {
			cmd = cmd + "-p ";
		}
		if (remoteTargetDir != null && remoteTargetDir.length() > 0) {
			cmd = cmd + "-d " + remoteTargetDir;
		}

		try {
			channel = getExecChannel();
			channel.setCommand(cmd);
			sendFile(channel, localFile, remoteTargetName, mode);
			channel.disconnect();
		} catch (JSchException e) {
			if (channel != null) {
				channel.disconnect();
			}
			e.printStackTrace();
			// LOGGER.error(e);
			throw new Exception("Error during SCP transfer." + e.getMessage());
		} catch (Exception e) {
			e.printStackTrace();
			// LOGGER.error(e);
			throw new Exception(e.getLocalizedMessage());
		}
	}

	/**
	 * Download a file from the remote server to a local file.
	 * 
	 * @param remoteFile
	 *            Path and name of the remote file.
	 * @param localTarget
	 *            Local file where to store the data. Must be absolute.
	 * @throws IOException
	 *             in case of network problems
	 * @throws Exception
	 *             in case of problems on the target system (connection ok)
	 */
	public void get(String remoteFile, String localTarget) throws Exception {
		try {
			File f = new File(localTarget);
			FileOutputStream fop = new FileOutputStream(f);
			get(remoteFile, fop);
		} catch (IOException e) {
			e.printStackTrace();
			// LOGGER.error(e);
			throw new Exception(e.getLocalizedMessage());
		}
	}

	/**
	 * Download a file from the remote server into an OutputStream
	 * 
	 * @param remoteFile
	 *            Path and name of the remote file.
	 * @param localTarget
	 *            OutputStream to store the data.
	 * @throws IOException
	 *             in case of network problems
	 * @throws Exception
	 *             in case of problems on the target system (connection ok)
	 */
	private void get(String remoteFile, OutputStream localTarget) throws IOException, Exception {
		ChannelExec channel = null;

		if ((remoteFile == null) || (localTarget == null)) {
			throw new IllegalArgumentException("Null argument.");
		}

		String cmd = "scp -p -f " + remoteFile;

		try {
			channel = getExecChannel();
			channel.setCommand(cmd);
			receiveStream(channel, remoteFile, localTarget);
			channel.disconnect();
		} catch (JSchException e) {
			if (channel != null) {
				channel.disconnect();
			}
			throw (IOException) new IOException("Error during SCP transfer." + e.getMessage()).initCause(e);
		}
	}

	/**
	 * Initiates an SCP sequence but stops after getting fileinformation header
	 * 
	 * @param remoteFile
	 *            to get information for
	 * @return the file information got
	 * @throws IOException
	 *             in case of network problems
	 * @throws Exception
	 *             in case of problems on the target system (connection ok)
	 */
	public FileInfo getFileinfo(String remoteFile) throws IOException, Exception {
		ChannelExec channel = null;
		FileInfo fileInfo = null;

		if (remoteFile == null) {
			throw new IllegalArgumentException("Null argument.");
		}

		String cmd = "scp -p -f \"" + remoteFile + "\"";

		try {
			channel = getExecChannel();
			channel.setCommand(cmd);
			fileInfo = receiveStream(channel, remoteFile, null);
			channel.disconnect();
		} catch (JSchException e) {
			throw (IOException) new IOException("Error during SCP transfer." + e.getMessage()).initCause(e);
		} finally {
			if (channel != null) {
				channel.disconnect();
			}
		}
		return fileInfo;
	}
}
